package org.eclipse.cdt.utils.pty;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * Imitates interactive terminal by buffering user input and echoing it.
 * 
 * Specifically adjusted to Windows cmd.exe to:
 * - immediately echo whatever user types
 * - translate \n to \r\n
 */
public class InputStreamProxy extends InputStream {

	private InputStream proxy;
	private OutputStream out;
	private byte[] inputBuffer = new byte[4096]; // TODO auto-grow buffers
	private int inputBufferLen = 0;
	private int inputBufferStart = 0;
	private int toUnecho = 0;
	
	private byte[] proxyBuffer = new byte[4096];
	private int proxyBufferLen = 0;
	private OutputStream echo;
	private Object inputAvailSem = new Object();
	private IOException ex;
	
	public InputStreamProxy(InputStream inputStream) {
		this.proxy = inputStream;
		out = new OutputStream() {
			
			@Override
			public void write(int b) throws IOException {
				inputBuffer[inputBufferLen] = (byte)b;
				inputBufferLen++;
				if (b == '\r') {
					inputBuffer[inputBufferLen++] = '\n';
					for (int i = 0; i < inputBufferLen; i++) {
						if (inputBuffer[i] == 8) { // backspace
							System.arraycopy(inputBuffer, i + 1, inputBuffer, i-1, inputBufferLen - i);
							inputBufferLen-=2;
							i-=2;
						}
					}
					echo.write(inputBuffer, 0, inputBufferLen);
					echo.flush();
					
					inputBufferLen = 0;
					inputBufferStart = 0;
				}
				synchronized (inputAvailSem) {
					inputAvailSem.notify();
				}
			}
		};
		
		Thread proxyWaiter = new Thread(new Runnable() {
			
			@Override
			public void run() {
				while (!proxiedStreamClosed()) {
					if (proxyBufferLen > 0) {
						try {
							Thread.sleep(100);
						} catch (InterruptedException e) {
							// empty
						}
					} else {
						try {
							proxyBufferLen = proxy.read(proxyBuffer, 0, proxyBuffer.length);
						} catch (IOException e) {
							ex = e;
						}
						
						synchronized (inputAvailSem) {
							inputAvailSem.notify();
						}
					}
				}
			}
			
		});
		proxyWaiter.start();
	}

	private boolean proxiedStreamClosed() {
		return proxyBufferLen == -1;
	}
	
	@Override
	public int available() throws IOException {
		return inputBufferLen > inputBufferStart ? inputBufferLen - inputBufferStart : proxyBufferLen;
	}
	
	@Override
	public int read() throws IOException {
		if (proxiedStreamClosed()) {
			return -1;
		}
		
		if (ex != null) {
			inputBufferLen = -1;
			throw ex;
		}
		
		waitInput();
		if (inputBufferLen > inputBufferStart) {
			return inputBuffer[inputBufferStart++];
		} else { // if (proxyBufferLen > proxyBufferStart){
			int c = proxyBuffer[0];
			shiftProxyBuffer(1);
			return c;
		}
	}
	
	@Override
	public int read(byte[] b, int off, int len) throws IOException {
		if (proxiedStreamClosed()) {
			return -1;
		}
		
		if (ex != null) {
			inputBufferLen = -1;
			throw ex;
		}
		
		waitInput();
		if (inputBufferLen > inputBufferStart) {
			int toRead = Math.min(inputBufferLen - inputBufferStart, len - off);
			System.arraycopy(inputBuffer, inputBufferStart, b, off, toRead);
			inputBufferStart += toRead;
			toUnecho += toRead;
			return toRead;
		} else { // if (proxyBufferLen > proxyBufferStart){
			unecho();
			nToRn();
			int toRead = Math.min(proxyBufferLen, len - off);
			System.arraycopy(proxyBuffer, 0, b, off, toRead);
			shiftProxyBuffer(toRead);
			return toRead;
		}
	}
	
	/**
	 * backspace the fake interactive echo, as now real echo will come
	 */
	private void unecho() {
		if (toUnecho == 0) {
			return;
		}
		
		System.arraycopy(proxyBuffer, 0, proxyBuffer, toUnecho, Math.min(proxyBufferLen, proxyBuffer.length-toUnecho));
		for (int i = 0; i < toUnecho; i++) {
			proxyBuffer[i] = 8;
		}
		proxyBufferLen = Math.min(proxyBufferLen + toUnecho, proxyBuffer.length);
		toUnecho = 0;
	}
	
	/**
	 * Converts "[^\r]\n" to "\r\n"
	 */
	private void nToRn() {
		for (int i = 1; i < proxyBufferLen; i++) {
			if (proxyBuffer[i] == '\n' && proxyBuffer[i-1] != '\r') {
				System.arraycopy(proxyBuffer, i, proxyBuffer, i+1, proxyBufferLen - i);
				proxyBuffer[i] = '\r';
				proxyBufferLen = Math.min(proxyBufferLen+1, proxyBuffer.length);
				i++;
			}
		}
	}
	
	private void shiftProxyBuffer(int offset) {
		System.arraycopy(proxyBuffer, offset, proxyBuffer, 0, proxyBuffer.length - offset);
		proxyBufferLen -= offset;
	}


	private void waitInput() {
		if (inputBufferLen > inputBufferStart) {
			return;
		}
		if (proxyBufferLen > 0) {
			return;
		}
		while (!proxiedStreamClosed()) {
			try {
				synchronized (inputAvailSem) {
					inputAvailSem.wait();
				}
			} catch (InterruptedException e) {
				// empty
			}
			
			if (inputBufferLen > inputBufferStart) {
				return;
			}
			if (proxyBufferLen > 0) {
				return;
			}
		}
	}


	@Override
	public void close() throws IOException {
		proxy.close();
	}
	
	@Override
	public synchronized void mark(int readlimit) {
	}
	
	@Override
	public boolean markSupported() {
		return false;
	}

	public void setEcho(OutputStream outputStream) {
		this.echo = outputStream;
	}

	public OutputStream getBufferedOutputStream() {
		return out;
	}

}
